package org.unidata.mdm.rest.v1.meta.service;

import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;

import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.cxf.jaxrs.ext.multipart.Attachment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.unidata.mdm.core.service.MetaModelService;
import org.unidata.mdm.core.type.model.ModelDescriptor;
import org.unidata.mdm.core.type.model.ModelInstance;
import org.unidata.mdm.core.type.security.Right;
import org.unidata.mdm.core.type.security.SecurityToken;
import org.unidata.mdm.core.type.security.User;
import org.unidata.mdm.core.util.SecurityUtils;
import org.unidata.mdm.meta.exception.MetaExceptionIds;
import org.unidata.mdm.rest.system.exception.NotAllowException;
import org.unidata.mdm.rest.system.service.AbstractRestService;
import org.unidata.mdm.rest.v1.meta.exception.MetaRestExceptionIds;
import org.unidata.mdm.system.exception.PlatformBusinessException;
import org.unidata.mdm.system.exception.PlatformFailureException;
import org.unidata.mdm.system.serialization.xml.XmlObjectSerializer;
import org.unidata.mdm.system.service.ExecutionService;
import org.unidata.mdm.system.service.RenderingService;

/**
 * Base meta model rest service
 *
 * @author Alexandr Serov
 * @since 23.11.2020
 **/
public abstract class AbstractMetaModelRestService extends AbstractRestService {

    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractMetaModelRestService.class);

    private static final String EXPORT_FILE_NAME_TEMPLATE = "%s-%s.xml";

    @Autowired
    protected MetaModelService metaModelService;

    @Autowired
    protected ExecutionService executionService;

    @Autowired
    protected RenderingService renderingService;

    protected Long resolveDraftId(Long id) {
        return id != null && (id > 0L) ? id : null;
    }

    protected void throwIfNotAllow(String topLevelName) throws NotAllowException {
        if (!allow(topLevelName)) {
            throw new NotAllowException();
        }
    }

    protected boolean allow(String topLevelName) {

        if (Objects.isNull(topLevelName)) {
            return false;
        }

        SecurityToken token = SecurityUtils.getSecurityTokenForCurrentUser();
        try {

            if (token == null || token.getUser().isAdmin()
                || token.getRightsMap().containsKey(SecurityUtils.ADMIN_SYSTEM_MANAGEMENT)
                || token.getRightsMap().containsKey(SecurityUtils.ADMIN_DATA_MANAGEMENT_RESOURCE_NAME)) {
                return true;
            }

            return token.getRightsMap().containsKey(topLevelName);
        } catch (Exception e) {
            throw new PlatformFailureException("Metadata service failed to retrieve data [{}].", e, MetaExceptionIds.EX_META_ENTITY_NOT_FOUND);
        }
    }

    protected Response exportXmlFile(String fileName, Object content) {
        String fileNameWithTimestamp = String.format(EXPORT_FILE_NAME_TEMPLATE, fileName, DateFormatUtils.format(System.currentTimeMillis(), "yyyy-MM-dd_HH-mm-ss"));
        try {
            final String encodedFilename = URLEncoder.encode(fileNameWithTimestamp, StandardCharsets.UTF_8.name());
            return Response.ok((StreamingOutput) output -> output.write(toXmlByteArray(content)))
                .encoding(StandardCharsets.UTF_8.name())
                .header("Content-Disposition", "attachment; filename=" + encodedFilename + "; filename*=UTF-8''" + encodedFilename)
                .header("Content-Type", MediaType.TEXT_XML)
                .build();
        } catch (UnsupportedEncodingException e) {
            throw new IllegalStateException("Unsupported Encoding: " + StandardCharsets.UTF_8.name());
        }
    }

    protected <T, R> R importXmlFile(Attachment attachment, Class<T> javaClass, Function<T, R> handler) {
        Objects.requireNonNull(attachment, "Attachment can't be null");
        MediaType contentType = attachment.getContentType();
        if (MediaType.TEXT_XML_TYPE.equals(contentType)) {
            return handler.apply(readFromAttachment(attachment, javaClass));
        } else {
            LOGGER.warn("Invalid content type rejected, while importing model. {}", attachment.getContentType());
            throw new PlatformBusinessException("Import of enumerations failed. Invalid media type [{}]. XML is expected.",
                MetaRestExceptionIds.EX_META_ENUMERATION_IMPORT_UNSUPPORTED, contentType.toString());
        }
    }

    protected <I extends ModelInstance<?>> I instance(ModelDescriptor<I> descriptor) {
        return instance(descriptor, null);
    }

    protected <I extends ModelInstance<?>> I instance(ModelDescriptor<I> descriptor, String storageId) {
        Objects.requireNonNull(descriptor, "ModelDescriptor can't be null");
        I result = storageId == null ? metaModelService.instance(descriptor) : metaModelService.instance(descriptor, storageId);
        if (result == null) {
            throw new IllegalStateException("Model descriptor not available: " + descriptor.getModelTypeId());
        }
        return result;
    }

    private <T> T readFromAttachment(Attachment attachment, Class<T> javaClass) {
        XmlObjectSerializer serializer = XmlObjectSerializer.getInstance();
        try {
            T typedObject = serializer.fromXmlInputStream(javaClass, attachment.getObject(InputStream.class));
            if (typedObject == null) {
                throw new IllegalStateException("Null result");
            }
            return typedObject;
        } catch (Exception ex) {
            throw new PlatformBusinessException("Unmarshaling failed (null result).", ex, MetaRestExceptionIds.EX_META_MARSHALING_FAILED);
        }
    }

    private byte[] toXmlByteArray(Object value) {
        XmlObjectSerializer serializer = XmlObjectSerializer.getInstance();
        try {
            return serializer.toXmlString(value, true).getBytes(StandardCharsets.UTF_8);
        } catch (Exception ex) {
            throw new PlatformBusinessException("Marshaling failed.", ex, MetaRestExceptionIds.EX_META_MARSHALING_FAILED);
        }
    }

}
